Skip to content

Fixes #27060: Fix TestCaseRepository deleteChildren O(N²) nested loop and double-delete in deleteAllTestCaseResults#27061

Open
RajdeepKushwaha5 wants to merge 4 commits intoopen-metadata:mainfrom
RajdeepKushwaha5:fix/testcase-repository-delete-children-n-squared-bug
Open

Fixes #27060: Fix TestCaseRepository deleteChildren O(N²) nested loop and double-delete in deleteAllTestCaseResults#27061
RajdeepKushwaha5 wants to merge 4 commits intoopen-metadata:mainfrom
RajdeepKushwaha5:fix/testcase-repository-delete-children-n-squared-bug

Conversation

@RajdeepKushwaha5
Copy link
Copy Markdown
Contributor

Describe your changes:

Fixes #27060

What changes did I make?

Fixed two bugs in TestCaseRepository.java:

Bug 1 — deleteChildren() O(N²) nested loop (lines 896–914):

The method had a nested loop where both the outer and inner loops iterated over the same children list. For N children, deleteById() was called N×N times instead of N — each child was deleted N times. Additionally:

  • TestCaseResolutionStatusRepository was re-instantiated on every outer iteration instead of once
  • The outer loop variable entityRelationshipRecord was only used for logging, never for deletion
  • The log message used hardDelete ? "hard" : "soft" but the enclosing if (hardDelete) guaranteed it always logged "hard"

Fix: Removed the outer loop entirely. Moved repository instantiation outside the loop. Single pass over children with one deleteById() call per child.

Bug 2 — deleteAllTestCaseResults() double-delete (lines 921–933):

The method called testCaseResultRepository.deleteAllTestCaseResults(fqn) synchronously, then submitted the exact same call asynchronously via asyncExecutor. This caused every test case result to be deleted twice — once immediately and once in a background thread (which would find nothing left to delete).

Fix: Removed the redundant async call. Kept only the synchronous call since this runs inside entitySpecificCleanup()cleanup() which operates within a transaction. Also removed the now-unused asyncExecutor field and its imports (ExecutorService, Executors).

Why did I make them?

  • Bug 1 causes O(N²) database operations when hard-deleting a TestCase with many resolution statuses, degrading performance significantly at scale
  • Bug 2 wastes database queries by executing identical DELETE statements twice, with a potential race condition between the sync and async calls

How did I test my changes?

  • Verified the base class EntityRepository.deleteChildren() uses a single loop (correct pattern)
  • Confirmed EntityTimeSeriesRepository.deleteById() is idempotent (returns early if not found), meaning the bug causes silent performance degradation rather than exceptions
  • Verified asyncExecutor had no other call sites in the file after removing the double-delete
  • Ran mvn spotless:check — passes clean

Type of change:

  • Bug fix
  • Improvement
  • New feature
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • I have commented on my code, particularly in hard-to-understand areas.
  • For JSON Schema changes: I updated the migration scripts or explained why it is not needed.
  • I have added a test that covers the exact scenario we are fixing. For complex issues, comment the issue number in the test for future reference.
    • The fix is a straightforward loop de-duplication. The deleteById() method is idempotent, so existing integration tests for TestCase hard-delete continue to validate the cleanup path. No new test is needed since the behavior (children get deleted) doesn't change — only the number of redundant DB calls does.

Copilot AI review requested due to automatic review settings April 5, 2026 23:47
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 5, 2026

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes performance and correctness issues in TestCaseRepository cleanup paths by removing redundant loops/calls that caused excessive and duplicated deletions during hard deletes.

Changes:

  • Fix deleteChildren() to delete each TestCaseResolutionStatus child exactly once (removes O(N²) nested-loop behavior and avoids re-instantiating the repository per iteration).
  • Fix deleteAllTestCaseResults() to avoid deleting the same test case results twice by removing the duplicate async submission.
  • Remove the now-unused async executor and related imports.

TestCaseResultRepository testCaseResultRepository =
(TestCaseResultRepository) Entity.getEntityTimeSeriesRepository(TEST_CASE_RESULT);
testCaseResultRepository.deleteAllTestCaseResults(fqn);
asyncExecutor.submit(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RajdeepKushwaha5 any reason you removed the async excecutor here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@harshach

The current code calls deleteAllTestCaseResults(fqn) twice — once synchronously and then the exact same call again via asyncExecutor.submit():

testCaseResultRepository.deleteAllTestCaseResults(fqn);     // deletes everything right here
asyncExecutor.submit(() -> {
    testCaseResultRepository.deleteAllTestCaseResults(fqn);  // runs later, finds nothing to delete
});

The sync call finishes and wipes all the results before the async task even starts, so the async one always ends up being a no-op — it just executes the same DELETEs against rows that no longer exist.

I kept the sync call and removed the async one (rather than the other way around) because deleteAllTestCaseResults() is called from entitySpecificCleanup(), which runs inside the JDBI transaction in cleanup(). The sync call participates in that transaction — so if anything downstream fails and the transaction rolls back, the results stay intact (consistent with the test case not being deleted). If we kept only the async call, it would run on a separate thread outside the transaction, meaning it could delete results even when the parent entity deletion rolled back.

So basically: the async call was redundant (double-delete), and the sync one is the correct one to keep for transactional safety.

Happy to restructure if you'd prefer a different approach — just let me know!

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 9, 2026

🟡 Playwright Results — all passed (16 flaky)

✅ 3671 passed · ❌ 0 failed · 🟡 16 flaky · ⏭️ 89 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 479 0 2 4
🟡 Shard 2 651 0 2 7
🟡 Shard 3 656 0 3 1
🟡 Shard 4 632 0 2 27
🟡 Shard 5 610 0 1 42
🟡 Shard 6 643 0 6 8
🟡 16 flaky test(s) (passed on retry)
  • Pages/AuditLogs.spec.ts › should apply both User and EntityType filters simultaneously (shard 1, 1 retry)
  • Pages/UserCreationWithPersona.spec.ts › Create user with persona and verify on profile (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Glossary (shard 2, 1 retry)
  • Features/DataProductRenameConsolidation.spec.ts › Rename then change owner - assets should be preserved (shard 2, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Features/RestoreEntityInheritedFields.spec.ts › Validate restore with Inherited domain and data products assigned (shard 3, 1 retry)
  • Features/RTL.spec.ts › Verify Following widget functionality (shard 3, 1 retry)
  • Pages/Customproperties-part2.spec.ts › entityReferenceList shows item count, scrollable list, no expand toggle (shard 4, 1 retry)
  • Pages/DataContracts.spec.ts › Create Data Contract and validate for MlModel (shard 4, 1 retry)
  • Pages/Glossary.spec.ts › Add and Remove Assets (shard 5, 1 retry)
  • Pages/HyperlinkCustomProperty.spec.ts › should display URL when no display text is provided (shard 6, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › Column lineage for table -> mlModel (shard 6, 2 retries)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 6, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: searchIndex (shard 6, 1 retry)
  • Pages/ODCSImportExport.spec.ts › Multi-object ODCS contract - object selector shows all schema objects (shard 6, 1 retry)
  • Pages/Users.spec.ts › Permissions for table details page for Data Consumer (shard 6, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Copilot AI review requested due to automatic review settings April 12, 2026 04:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings April 20, 2026 08:47
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Apr 20, 2026

Code Review ✅ Approved

Eliminates the O(N²) nested loop in TestCaseRepository.deleteChildren and removes the double-delete bug in deleteAllTestCaseResults. No issues found.

Options

Display: compact → Showing less information.

Comment with these commands to change:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] TestCaseRepository.deleteChildren() has O(N²) nested loop bug causing redundant deletes

3 participants